#include "Exceptions.h"
#include <fstream>
#include "ShaderPrograms.h"

using namespace cagd;
using namespace std;

ShaderProgram::ShaderProgram():
        _program(0), _linked(0)
{
}

// returns true if an OpenGL error occurred, false otherwise.
GLboolean ShaderProgram::_ListOpenGLErrors(const char *file_name, GLint line, ostream& output) const
{
    GLenum gl_error;
    GLboolean result = false;

    gl_error = glGetError();
        output << "\t\\begin{OpenGL Errors}" << endl;
        
    while (gl_error != GL_NO_ERROR)
    {
        output << "\t\tError in file " << file_name << " at line " << line << ": " << endl << gluErrorString(gl_error) << endl;
        result = true;
        gl_error = glGetError();
    }
    
    output << "\t\\end{OpenGL Errors}" << endl << endl;
    return result;
}

GLvoid ShaderProgram::_ListVertexShaderInfoLog(GLuint index, std::ostream &output) const
{
    GLint info_log_length = 0;
    GLint chars_written  = 0;
    GLchar *info_log = 0;

    // check for OpenGL errors
    _ListOpenGLErrors(__FILE__, __LINE__, output);

    glGetShaderiv(_vertex_shaders[index], GL_INFO_LOG_LENGTH, &info_log_length);
    if (info_log_length > 0)
    {
        info_log = new GLchar[info_log_length];
        if (!info_log)
            throw Exception("ShaderProgram::_ListVertexShaderInfoLog - Could not allocate information log buffer!");

        glGetShaderInfoLog(_vertex_shaders[index], info_log_length, &chars_written, info_log);

        output << "\t\\begin{Vertex Shader Information Log}" << endl << "\t\tid = " << _vertex_shaders[index] << ", name = " << _vertex_shader_file_names[index] << endl;
        output <<  "\t\t" << info_log << endl;
        output << "\t\\end{Vertex Shader Information Log}" << endl << endl;

        delete[] info_log;
    }

    // check for OpenGL errors
    _ListOpenGLErrors(__FILE__, __LINE__, output);
}

GLvoid ShaderProgram::_ListFragmentShaderInfoLog(GLuint index, std::ostream &output) const
{
    GLint info_log_length = 0;
    GLint chars_written  = 0;
    GLchar *info_log = 0;

    // check for OpenGL errors
    _ListOpenGLErrors(__FILE__, __LINE__, output);

    glGetShaderiv(_fragment_shaders[index], GL_INFO_LOG_LENGTH, &info_log_length);

    if (info_log_length > 0)
    {
        info_log = new GLchar[info_log_length];
        if (!info_log)
            throw Exception("ShaderProgram::_ListFragmentShaderInfoLog - Could not allocate information log buffer!");

        glGetShaderInfoLog(_fragment_shaders[index], info_log_length, &chars_written, info_log);

        output << "\t\\begin{Fragment Shader InfoLog}" << endl<< "\t\tid = " << _fragment_shaders[index] << ", name = "  << _fragment_shader_file_names[index] << endl;
        output <<  "\t\t" << info_log << endl;
        output << "\t\\end{Fragment Shader InfoLog}" << endl << endl;

        delete[] info_log;
    }

    // check for OpenGL errors
    _ListOpenGLErrors(__FILE__, __LINE__, output);
}

GLvoid ShaderProgram::_ListProgramInfoLog(ostream& output) const
{
    GLint info_log_length = 0;
    GLint chars_written  = 0;
    GLchar *info_log = 0;

    // check for OpenGL errors
    _ListOpenGLErrors(__FILE__, __LINE__, output);

    glGetProgramiv(_program, GL_INFO_LOG_LENGTH, &info_log_length);

    if (info_log_length > 0)
    {
        info_log = new GLchar[info_log_length];
        if (!info_log)
            throw Exception("ShaderProgram::_ListProgramInfoLog - Could not allocate information log buffer!");

        glGetProgramInfoLog(_program, info_log_length, &chars_written, info_log);

        output << "\t\\begin{Program InfoLog}" << endl << "\t\tid = " << _program << endl;
        output <<  "\t\t" << info_log << endl;
        output << "\t\\end{Program InfoLog}" << endl << endl;

        delete[] info_log;
    }

    // check for OpenGL errors
    _ListOpenGLErrors(__FILE__, __LINE__, output);
}

GLvoid ShaderProgram::_ListValidateInfoLog(ostream& output) const
{
    GLint status = false;

    // check for OpenGL errors
    _ListOpenGLErrors(__FILE__, __LINE__, output);

    glGetProgramiv(_program, GL_VALIDATE_STATUS, &status);

    output << "\t\\begin{Program Validate InfoLog}" << endl<< "\t\tid = " << _program << endl;
    output <<  (status? "\t\tValidated." : "\t\tNot validated.") << endl;
    output << "\t\\end{Program Validate InfoLog}" << endl << endl;

    // check for OpenGL errors
    _ListOpenGLErrors(__FILE__, __LINE__, output);
}

GLint ShaderProgram::GetUniformVariableLocation(const GLchar *name, GLboolean logging_is_enabled, ostream& output) const
{
    GLint loc = glGetUniformLocation(_program, name);

    if (loc == -1)
    {
        string reason = "\t\tNo such uniform named: ";
        reason += name;
        output << reason << endl;

        // check for OpenGL errors
        if (logging_is_enabled)
            _ListOpenGLErrors(__FILE__, __LINE__, output);
    }
    return loc;
}

GLvoid ShaderProgram::CreateProgram(GLboolean logging_is_enabled, std::ostream &output)
{
    // 5) creating the program object
    {
        if (logging_is_enabled)
        {
            output << "Creating the program object..." << endl;
            output << "------------------------------" << endl;
        }

        _program = glCreateProgram();

        if (logging_is_enabled)
            output << "Done." << endl << endl;
    }
}

GLboolean ShaderProgram::SetUniformVariable1i(const GLchar *name, GLint parameter) const
{
    if (!_program)
        return false;

    GLint location = GetUniformVariableLocation(name);
    if (location == -1)
        return false;

    glUniform1i(location, parameter);

    return true;
}

GLboolean ShaderProgram::SetUniformVariable1f(const GLchar *name, GLfloat parameter) const
{
    if (!_program)
        return false;

    GLint location = GetUniformVariableLocation(name);
    if (location == -1)
        return false;

    glUniform1f(location, parameter);

    return true;
}

GLboolean ShaderProgram::SetUniformVariable2f(const GLchar *name, GLfloat parameter_1, GLfloat parameter_2) const
{
    if (!_program)
        return false;

    GLint location = GetUniformVariableLocation(name);
    if (location == -1)
            return false;

    glUniform2f(location, parameter_1, parameter_2);

    return true;
}

GLboolean ShaderProgram::SetUniformVariable3f(const GLchar *name, GLfloat parameter_1, GLfloat parameter_2, GLfloat parameter_3) const
{
    if (!_program)
        return false;

    GLint location = GetUniformVariableLocation(name);
    if (location == -1)
            return false;

    glUniform3f(location, parameter_1, parameter_2, parameter_3);

    return true;
}

GLvoid ShaderProgram::Disable() const
{
    glUseProgram(0);
}

GLvoid ShaderProgram::Enable(GLboolean logging_is_enabled, ostream& output) const
{
    if (_linked)
    {
        glUseProgram(_program);
        glValidateProgram(_program);

        if (logging_is_enabled)
            _ListValidateInfoLog(output);
    }
}

ShaderProgram::~ShaderProgram()
{
    for (vector<GLuint>::iterator it = _fragment_shaders.begin(); it != _fragment_shaders.end(); it++)
        glDeleteShader(*it);
    for (vector<GLuint>::iterator it = _vertex_shaders.begin(); it != _vertex_shaders.end(); it++)
        glDeleteShader(*it);
    if (_program)
        glDeleteProgram(_program);
}

GLboolean ShaderProgram::AddFragmentShader(GLuint index, const std::string &file_name, GLboolean logging_is_enabled, std::ostream &output)
{
    if (index >= _fragment_shaders.size())
    {
        _fragment_shaders.resize(index + 1);
        _fragment_shader_compiled.resize(index + 1);
        _fragment_shader_file_names.resize(index + 1);
        _fragment_shader_sources.resize(index + 1);
    }
    // save file_name
    _fragment_shader_file_names[index] = file_name;
    // get reference to the source storage
    string &source = _fragment_shader_sources[index];
    source = "";
    string aux = "";

    fstream file(file_name.c_str(), ios_base::in);

    if (logging_is_enabled)
    {
        output << "Source of '" << file_name << "'' fragment shader" << endl;
        output << "-------------------------" << endl;
    }

    while (!file.eof())
    {
            getline(file, aux, '\n');
            source += aux + '\n';

            if (logging_is_enabled)
                output << "\t" << aux << endl;
    }

    file.close();

    if (logging_is_enabled)
        output << endl;

    // create new shader
    if (logging_is_enabled)
    {
        output << "Creating empty fragment shader object..." << endl;
        output << "----------------------------------------------------" << endl;
    }

    _fragment_shaders[index] = glCreateShader(GL_FRAGMENT_SHADER);

    if (logging_is_enabled)
        output << "Done." << endl << endl;

    if (logging_is_enabled)
    {
        output << "Setting the source codes for the shader..." << endl;
        output << "-------------------------------------------" << endl;
    }

    const GLchar *pointer_to_fragment_shader_source = &_fragment_shader_sources[index][0];
    glShaderSource(_fragment_shaders[index], 1, &pointer_to_fragment_shader_source, NULL);

    if (logging_is_enabled)
    {
        // check for OpenGL errors
        _ListOpenGLErrors(__FILE__, __LINE__, output);

        output << "Done." << endl << endl;
    }

    if (logging_is_enabled)
    {
        output << "Compiling the fragment shader..." << endl;
        output << "--------------------------------" << endl;
    }

    glCompileShader(_fragment_shaders[index]);
    glGetShaderiv(_fragment_shaders[index], GL_COMPILE_STATUS, &_fragment_shader_compiled[index]);

    if (logging_is_enabled)
    {
        _ListFragmentShaderInfoLog(index, output);
        output << (_fragment_shader_compiled[index] ? "\tSuccesful." : "\tUnsuccesful.") << endl << "Done." << endl << endl;
    }

    if (!_fragment_shader_compiled[index])
    {
        glDeleteShader(_fragment_shaders[index]);
        _fragment_shaders[index] = 0;
        _fragment_shader_compiled[index] = false;
        _fragment_shader_file_names[index] = "";
        _fragment_shader_sources[index] = "";
        return false;
    }

    return true;
}

GLvoid ShaderProgram::RemoveFragmentShader(GLuint index)
{
    if (index <= _fragment_shaders.size())
        return;
    glDeleteShader(_fragment_shaders[index]);
    _fragment_shaders[index] = 0;
    _fragment_shader_compiled[index] = false;
    _fragment_shader_file_names[index] = "";
    _fragment_shader_sources[index] = "";
}

GLboolean ShaderProgram::AddVertexShader(GLuint index, const std::string &file_name, GLboolean logging_is_enabled, std::ostream &output)
{
    if (index >= _vertex_shaders.size())
    {
        _vertex_shaders.resize(index + 1);
        _vertex_shader_compiled.resize(index + 1);
        _vertex_shader_file_names.resize(index + 1);
        _vertex_shader_sources.resize(index + 1);
    }
    // save file_name
    _vertex_shader_file_names[index] = file_name;
    // get reference to the source storage
    string &source = _vertex_shader_sources[index];
    source = "";
    string aux = "";

    if (logging_is_enabled)
    {
        output << "Source of '" << file_name << "' vertex shader" << endl;
        output << "-----------------------" << endl;
    }

    fstream file(file_name.c_str());

    while (!file.eof())
    {
        getline(file, aux, '\n');
        source += aux + '\n';

        if (logging_is_enabled)
            output << "\t" << aux << endl;
    }

    file.close();

    if (logging_is_enabled)
        output << endl;

    // create new shader
    if (logging_is_enabled)
    {
        output << "Creating empty vertex shader object..." << endl;
        output << "----------------------------------------------------" << endl;
    }

    _vertex_shaders[index] = glCreateShader(GL_VERTEX_SHADER);

    if (logging_is_enabled)
        output << "Done." << endl << endl;

    if (logging_is_enabled)
    {
        output << "Setting the source code for the shader..." << endl;
        output << "-------------------------------------------" << endl;
    }

    const GLchar *pointer_to_vertex_shader_source = &_vertex_shader_sources[index][0];
    glShaderSource(_vertex_shaders[index], 1, &pointer_to_vertex_shader_source, NULL);

    if (logging_is_enabled)
    {
        // check for OpenGL errors
        _ListOpenGLErrors(__FILE__, __LINE__, output);

        output << "Done." << endl << endl;
    }

    if (logging_is_enabled)
    {
        output << "Compiling the vertex shader..." << endl;
        output << "------------------------------" << endl;
    }

    glCompileShader(_vertex_shaders[index]);
    glGetShaderiv(_vertex_shaders[index], GL_COMPILE_STATUS, &_vertex_shader_compiled[index]);

    if (logging_is_enabled)
    {
        _ListVertexShaderInfoLog(index, output);
        output << (_vertex_shader_compiled[index] ? "\tSuccesful." : "\tUnsuccesful.") << endl << "Done." << endl << endl;
    }

    if (!_vertex_shader_compiled[index])
    {
        glDeleteShader(_vertex_shaders[index]);
        return false;
    }

    return true;
}

GLvoid ShaderProgram::RemoveVertexShader(GLuint index)
{
    if (index <= _vertex_shaders.size())
        return;
    glDeleteShader(_vertex_shaders[index]);
    _vertex_shaders[index] = 0;
    _vertex_shader_compiled[index] = false;
    _vertex_shader_file_names[index] = "";
    _vertex_shader_sources[index] = "";
}

GLboolean ShaderProgram::AttachFragmentShader(GLuint index, GLboolean logging_is_enabled, std::ostream &output)
{
    if (index >= _fragment_shaders.size() ||
        (!_fragment_shader_compiled[index] && _fragment_shaders[index] == 0))
        return false;

    if (!_fragment_shader_compiled[index])
    {
        if (logging_is_enabled)
        {
            output << "Compiling the fragment shader..." << endl;
            output << "------------------------------" << endl;
        }

        glCompileShader(_fragment_shaders[index]);
        glGetShaderiv(_fragment_shaders[index], GL_COMPILE_STATUS, &_fragment_shader_compiled[index]);

        if (logging_is_enabled)
        {
            _ListFragmentShaderInfoLog(index, output);
            output << (_fragment_shader_compiled[index] ? "\tSuccesful." : "\tUnsuccesful.") << endl << "Done." << endl << endl;
        }

        if (!_fragment_shader_compiled[index])
        {
            glDeleteShader(_fragment_shaders[index]);
            return false;
        }
    }

    if (logging_is_enabled)
        output << "\tAttaching vertex and fragment shaders to the program object..." << endl;

    glAttachShader(_program, _fragment_shaders[index]);

    // check for OpenGL errors
    if (logging_is_enabled)
    {
        _ListOpenGLErrors(__FILE__, __LINE__, output);
        output << "Done." << endl << endl;
    }

    return true;
}

GLvoid ShaderProgram::DettachFragmentShader(GLuint index)
{
    if (index >= _fragment_shaders.size())
        return;

    glDetachShader(_program, _fragment_shaders[index]);
}

GLboolean ShaderProgram::AttachVertexShader(GLuint index, GLboolean logging_is_enabled, std::ostream &output)
{
    if (index >= _vertex_shaders.size() ||
        (!_vertex_shader_compiled[index] && _vertex_shaders[index] == 0))
        return false;

    if (!_vertex_shader_compiled[index])
    {
        if (logging_is_enabled)
        {
            output << "Compiling the vertex shader..." << endl;
            output << "------------------------------" << endl;
        }

        glCompileShader(_vertex_shaders[index]);
        glGetShaderiv(_vertex_shaders[index], GL_COMPILE_STATUS, &_vertex_shader_compiled[index]);

        if (logging_is_enabled)
        {
            _ListVertexShaderInfoLog(index, output);
            output << (_vertex_shader_compiled[index] ? "\tSuccesful." : "\tUnsuccesful.") << endl << "Done." << endl << endl;
        }

        if (!_vertex_shader_compiled[index])
        {
            glDeleteShader(_vertex_shaders[index]);
            return false;
        }
    }

    if (logging_is_enabled)
        output << "\tAttaching vertex and fragment shaders to the program object..." << endl;

    glAttachShader(_program, _vertex_shaders[index]);

    // check for OpenGL errors
    if (logging_is_enabled)
    {
        _ListOpenGLErrors(__FILE__, __LINE__, output);
        output << "Done." << endl << endl;
    }

    return true;
}

GLvoid ShaderProgram::DettachVertexShader(GLuint index)
{
    if (index >= _vertex_shaders.size())
        return;

    glDetachShader(_program, _vertex_shaders[index]);
}

GLboolean ShaderProgram::LinkProgram(GLboolean logging_is_enabled, std::ostream &output)
{
    if (logging_is_enabled)
        output << "\tLinking the program..." << endl;

    glLinkProgram(_program);
    glGetProgramiv(_program, GL_LINK_STATUS, &_linked);

    if (logging_is_enabled)
    {
        _ListProgramInfoLog(output);
        output << (_linked ? "\tSuccesful." : "\tUnsuccesful.") << endl << "Done." << endl << endl;
    }

    return _linked;
}
